01 - 记忆与上下文的边界
前置:无。本篇是专题起点。
本篇回答:什么算记忆、什么不算,以及在什么情况下你不需要这一层。
本篇会用到的词:
| 词 | 意思 |
|---|---|
| 会话(session) | 一次连续的交互。会话内多轮共享同一个 messages 数组,会话结束这个数组通常被丢弃 |
| 语料库(corpus) | RAG 检索的对象,事先准备好的一批文档。它跟具体某个用户无关 |
| Profile(画像) | 一种记忆存储形态:每个用户一份结构化文档,新事实覆盖旧字段 |
| Collection(集合) | 另一种形态:每条记忆是一条独立记录,只增不改,靠检索挑出相关的几条 |
| 检查点(checkpoint) | 持久化执行保存的执行状态快照。它记录"执行到哪一步",不记录"学到了什么" |
一、没有记忆层时,具体是什么样
一个订餐助手,两次会话隔了六天:
# 第一次会话(周一)—— 用户主动说了一件重要的事
messages = [
{"role": "user", "content": "帮我订个餐,我对花生过敏"},
{"role": "assistant", "content": "好的,我会避开含花生的菜品……"},
]
# 会话结束,这个 list 被丢弃。它存在于进程内存或某张会话表里,
# 但下一次请求不会带上它 —— 带上就意味着上下文无限增长。
# 第二次会话(周日)—— 全新的 messages,模型这边一个字节的历史都没有
messages = [
{"role": "user", "content": "推荐个川菜馆"},
]
# 模型不是"忘了"花生过敏,是这次请求里根本没有这个信息。
# 它推荐宫保鸡丁没有任何异常 —— 在它收到的输入里,这是一个完全正常的推荐。
朴素的两种补法都不成立:
- 把全部历史都带上。六天里可能有几十轮,token 成本线性增长,且真正相关的那一句被淹没在无关内容里 —— 这是上下文工程专题反复处理的问题
- 让用户每次重说一遍。这等于把状态管理的责任推给用户,而"记住我说过的话"恰恰是助手类产品的核心体验
记忆层做的是第三件事:在第一次会话结束后,把"用户对花生过敏"这一条单独存下来;第二次会话开始时,只把这一条(以及其他几条相关的)放回上下文。
二、四个常被混作一谈的东西
2.1 记忆和 RAG 的判据:换个用户还成不成立
同一段文本,归属哪一层取决于它跟用户的关系:
| 内容 | 归属 | 理由 |
|---|---|---|
| 「退款政策是七天无理由」 | RAG | 对所有用户都一样,改一次全体生效 |
| 「这个用户上次退款被拒过」 | 记忆 | 换个用户就不成立 |
| 「产品手册第 3 章」 | RAG | 公共语料,用户没参与生产 |
| 「用户偏好深色模式」 | 记忆 | 由这个用户的交互产生 |
边界情况:企业知识库里"每个部门自己的规章"。它对个人不特定,但对群体特定。实践中按访问控制切分的语料走 RAG,把"这个用户属于哪个部门"作为一条记忆或直接作为请求参数 —— 不要为了少建一层,把整个部门知识库塞进记忆里。
2.2 记忆和持久化执行的判据:任务结束后还有没有用
持久化执行保存的是"这个长任务跑到第几步、中间变量是什么",任务一结束这份快照就没有价值了。记忆保存的是"从这个任务里学到的、下次还用得上的东西"。
两者在一个具体场景里同时出现:一个跑了四小时的数据清洗任务在第三小时崩了。
- 检查点让它从第三小时接着跑,而不是从头开始 —— 任务完成后这份检查点可以删
- 记忆记下的是"这个用户的 CSV 一律是 GBK 编码" —— 下一个任务还用得上
把后者写进检查点是常见错误:检查点按任务生命周期清理,这条经验会跟着任务一起被删掉。
三、三种记忆分型
分型不是学术分类,它决定了存储形态和更新策略。LangMem 的概念文档给出的三分法是目前引用最广的:
3.1 Profile 与 Collection:同一类事实的两种存法
语义记忆有两种存储形态,取舍很实在:
# 形态 A:Profile —— 每个用户一份结构化文档,字段固定
# 新事实进来是「改字段」,天然不会有两条矛盾的记录
profile = {
"user_id": "u_1024",
"dietary_restrictions": ["花生过敏"], # 新增一条过敏源就是往这个 list 里加
"preferred_cuisine": "川菜", # 口味变了就是覆盖这个字段
}
# 代价:字段是你事先定死的。用户说了一件你没设计过的事
#(「我周三晚上从不外食」),Profile 里没地方放,只能丢掉或塞进 notes 兜底字段。
# 形态 B:Collection —— 每条记忆一条记录,只增不改
collection = [
{"id": "m_01", "text": "用户对花生过敏", "created_at": "2026-08-18"},
{"id": "m_02", "text": "用户周三晚上从不外食", "created_at": "2026-08-19"},
{"id": "m_03", "text": "用户偏好川菜", "created_at": "2026-08-19"},
]
# 好处:任何事实都装得下,不需要事先设计 schema。
# 代价:① 读的时候要检索(Profile 是直接按 user_id 取整份)
# ② 会出现「用户偏好川菜」和「用户改吃粤菜了」两条并存 —— 冲突消解成为必需
实践中的分工:变化慢、字段可枚举的走 Profile,其余走 Collection。supermemory 这类产品把两者都做了,对外表现成"稳定事实 + 近期活动"两段拼接。
3.2 程序记忆为什么最危险
语义记忆写错了,最坏结果是模型多知道一条假事实,通常会被后续对话纠正。程序记忆写错了,改的是系统提示词 —— 它影响的是这个 Agent 之后所有会话的行为,而且没有任何一轮对话会去纠正它。
一个真实形态的例子:用户抱怨"你回复太啰嗦了",程序记忆把"回复要简短"写进系统提示词。三周后,一个需要详细说明的场景下,Agent 给出了一句话的回答,没人知道这是三周前那条记忆在起作用。
这是为什么绝大多数生产系统只做语义记忆:程序记忆需要配套的版本管理和回归评测,成本高一个数量级。